#include <cstdio>
#include <cstdlib>

// Included files for OpenGL Rendering
#ifdef __APPLE__
#include <OpenGL/gl.h>
#include <OpenGL/glu.h>
#include <GLUT/glut.h>
#else
#include <GL/glut.h>
#include <GL/gl.h>
#include <GL/glu.h>
#endif

#include "glCanvas.h"
#include "boundingbox.h"
#include "camera.h"
#include "radiosity.h"
#include "raytracer.h"
#include "mesh.h"
#include "raytree.h"
#include "utils.h"
#include <time.h>

// Timing
clock_t start, end;

// Menu system
int optionID;
char str[80];
char num[5];

// Menu IDs
#define BOUNCE   0
#define SHADOW   1
#define MOTION   2
#define AA       3
#define DOF_S    4
#define DOF_BR   5
#define DOF_FL   6

// ========================================================
// static variables of GLCanvas class

int GLCanvas::display_list_index;
ArgParser* GLCanvas::args;
Mesh* GLCanvas::mesh;
Radiosity* GLCanvas::radiosity;
RayTracer* GLCanvas::raytracer;
Image GLCanvas::image;

// State of the mouse cursor
int GLCanvas::mouseButton;
int GLCanvas::mouseX;
int GLCanvas::mouseY;
bool GLCanvas::shiftPressed;
bool GLCanvas::controlPressed;
bool GLCanvas::altPressed;

// params for the raytracing animation
int GLCanvas::raytracing_x;
int GLCanvas::raytracing_y;
int GLCanvas::raytracing_skip;

double GLCanvas::max_d;

// ========================================================
// Initialize all appropriate OpenGL variables, set
// callback functions, and start the main event loop.
// This function will not return but can be terminated
// by calling 'exit(0)'
// ========================================================

void GLCanvas::initialize(ArgParser *_args, Mesh *_mesh, 
			  RayTracer *_raytracer, Radiosity *_radiosity) {

  args = _args;
  mesh = _mesh;
  raytracer = _raytracer;
  radiosity = _radiosity;
  image = Image("output.ppm");
  image.Allocate(args->width, args->height);
  max_d = (double)std::max(args->width, args->height);

  // Setup menu stuff
  optionID = 0;

  // setup glut stuff
  glutInitWindowSize(args->width, args->height);
  glutInitWindowPosition(100,100);
  glutInitDisplayMode(GLUT_DOUBLE | GLUT_DEPTH | GLUT_RGB);
  glutCreateWindow("OpenGL Viewer");

  // basic rendering 
  glEnable(GL_LIGHTING);
  glEnable(GL_DEPTH_TEST);
  glEnable(GL_NORMALIZE);
  glShadeModel(GL_SMOOTH);
  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
  GLfloat ambient[] = { 0.2, 0.2, 0.2, 1.0 };
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient); 
  glLightModeli(GL_LIGHT_MODEL_LOCAL_VIEWER, GL_TRUE);
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_FALSE);
  glCullFace(GL_BACK);
  glEnable(GL_CULL_FACE);

  display_list_index = glGenLists(1);

  // Initialize callback functions
  glutMouseFunc(mouse);
  glutMotionFunc(motion);
  glutDisplayFunc(display);
  glutReshapeFunc(reshape);
  glutKeyboardFunc(keyboard);
  glutIdleFunc(idle);

  Render();

  // Enter the main rendering loop
  glutMainLoop();
}

// ========================================================

void GLCanvas::InitLight() {
  // Set the last component of the position to 0 to indicate
  // a directional light source

  GLfloat position[4] = { 30,30,100, 1};
  GLfloat diffuse[4] = { 0.75,0.75,0.75,1};
  GLfloat specular[4] = { 0,0,0,1};
  GLfloat ambient[4] = { 0.2, 0.2, 0.2, 1.0 };

  GLfloat zero[4] = {0,0,0,0};
  glLightfv(GL_LIGHT1, GL_POSITION, position);
  glLightfv(GL_LIGHT1, GL_DIFFUSE, diffuse);
  glLightfv(GL_LIGHT1, GL_SPECULAR, specular);
  glLightfv(GL_LIGHT1, GL_AMBIENT, zero);
  glEnable(GL_LIGHT1);
  glLightModeli(GL_LIGHT_MODEL_TWO_SIDE, GL_TRUE);
  glEnable(GL_COLOR_MATERIAL);
  glLightModelfv(GL_LIGHT_MODEL_AMBIENT, ambient);

  GLfloat spec_mat[4] = {1,1,1,1};
  float glexponent = 30;
  glMaterialfv(GL_FRONT, GL_SHININESS, &glexponent);
  glMaterialfv(GL_FRONT, GL_SPECULAR, spec_mat);

  glColorMaterial(GL_FRONT, GL_AMBIENT_AND_DIFFUSE);
  float back_color[] = { 0.2,0.8,0.8,1};
  glMaterialfv(GL_BACK, GL_AMBIENT_AND_DIFFUSE, back_color);
  glEnable(GL_LIGHT1);
}

void drawOption(int id, char *text, int value)
{
	if (optionID == id)
	{
		printf(">> ");
	}
	else
	{
		printf("   ");
	}

	printf("%s %d\n", text, value);
}

void drawOption(int id, char *text, float value)
{
	if (optionID == id)
	{
		printf(">> ");
	}
	else
	{
		printf("   ");
	}

	printf("%s %g\n", text, value);
}


void drawOption(int id, char *text, bool value)
{
	if (optionID == id)
	{
		printf(">> ");
	}
	else
	{
		printf("   ");
	}

	if (value)
	{
		printf("%s On\n", text);
	}
	else
	{
		printf("%s Off\n", text);
	}
}

void GLCanvas::displayText(void)
{
	system("CLS");

	printf("   Ray Tracing Parameters:\n\n");

	drawOption(0, "Bounce Count", args->num_bounces);
	drawOption(1, "Shadow Samples", args->num_shadow_samples);
	drawOption(2, "Motion Blur Samples", args->num_motion_samples);
	drawOption(3, "Adaptive Antialiasing", args->adaptive_antialiasing);
	drawOption(4, "Depth of Field Samples", args->num_dof_samples);
	drawOption(5, "DOF Blur Radius", args->dof_blur_radius);
	drawOption(6, "DOF Focal Length", args->dof_focal_length);

	printf("\n");
}

void GLCanvas::display(void)
{
	glDrawBuffer(GL_BACK);

	const Vec3f &bg = mesh->getBackgroundColor();

	// Clear the display buffer, set it to the background color
	glClearColor(bg.r(),bg.g(),bg.b(),0);
	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

	// Set the camera parameters
	mesh->getCamera()->glInit(args->width, args->height);
	glMatrixMode(GL_MODELVIEW);
	glLoadIdentity();
	mesh->getCamera()->glPlaceCamera();
	InitLight(); // light will be a headlamp!

	glEnable(GL_CULL_FACE);

	glCallList(display_list_index);
	HandleGLError(); 
   
	// Swap the back buffer with the front buffer to display
	// the scene
	glutSwapBuffers();
}

// ========================================================
// Callback function for window resize
// ========================================================

void GLCanvas::reshape(int w, int h) {
  args->width = w;
  args->height = h;
  max_d = (double)std::max(args->width, args->height);

  image.Allocate(w, h);

  // Set the OpenGL viewport to fill the entire window
  glViewport(0, 0, (GLsizei)args->width, (GLsizei)args->height);

  // Set the camera parameters to reflect the changes
  mesh->getCamera()->glInit(args->width, args->height);
}

// ========================================================
// Callback function for mouse click or release
// ========================================================

void GLCanvas::mouse(int button, int state, int x, int y) {
  args->raytracing_animation = false;
  // Save the current state of the mouse.  This will be
  // used by the 'motion' function
  mouseButton = button;
  mouseX = x;
  mouseY = y;
  shiftPressed = glutGetModifiers() & GLUT_ACTIVE_SHIFT;
  controlPressed = glutGetModifiers() & GLUT_ACTIVE_CTRL;
  altPressed = glutGetModifiers() & GLUT_ACTIVE_ALT;
}

// ========================================================
// Callback function for mouse drag
// ========================================================

void GLCanvas::motion(int x, int y) {
  // Control or Shift or Alt pressed = zoom
  // (don't move the camera, just change the angle or image size)
  if (controlPressed || shiftPressed || altPressed) {
    mesh->getCamera()->zoomCamera(mouseY-y);
  }
  // Left button = rotation
  // (rotate camera around the up and horizontal vectors)
  else if (mouseButton == GLUT_LEFT_BUTTON) {
    mesh->getCamera()->rotateCamera(0.005*(mouseX-x), 0.005*(mouseY-y));
  }
  // Middle button = translation
  // (move camera perpendicular to the direction vector)
  else if (mouseButton == GLUT_MIDDLE_BUTTON) {
    mesh->getCamera()->truckCamera(mouseX-x, y-mouseY);
  }
  // Right button = dolly or zoom
  // (move camera along the direction vector)
  else if (mouseButton == GLUT_RIGHT_BUTTON) {
    mesh->getCamera()->dollyCamera(mouseY-y);
  }
  mouseX = x;
  mouseY = y;

  // Redraw the scene with the new camera parameters
  glutPostRedisplay();
}

// ========================================================
// Callback function for keyboard events
// ========================================================

void GLCanvas::keyboard(unsigned char key, int x, int y) {
  args->raytracing_animation = false;

  	int i, j;

	switch (key)
	{
		// RAYTRACING STUFF
		case 'r':
			// Quick Render
			args->raytracing_animation = !args->raytracing_animation;

			if (args->raytracing_animation)
			{
				start = clock();

				raytracing_skip = std::max(args->width,args->height) / 5;
				if (raytracing_skip % 2 == 0) raytracing_skip++;
				assert (raytracing_skip >= 1);
				raytracing_x = raytracing_skip/2;
				raytracing_y = raytracing_skip/2;

				printf ("Quick render started, press 'R' to stop\n");
			}
			else
			{
				printf ("Quick render stopped, press 'R' to start\n");  
			}
			break;
		case 'f':
			// Full Render
			args->raytracing_animation = !args->raytracing_animation;

			if (args->raytracing_animation)
			{
				start = clock();

				raytracing_skip = 1;
				if (raytracing_skip % 2 == 0) raytracing_skip++;
				assert (raytracing_skip >= 1);
				raytracing_x = raytracing_skip/2;
				raytracing_y = raytracing_skip/2;

				printf ("Full render started, press 'F' to stop\n");
			}
			else
			{
				printf ("Full render stopped, press 'F' to start\n");  
			}
			break;
		case 'o':
			args->raytracing_output = !args->raytracing_output;

			if (args->raytracing_output)
			{
				start = clock();

				// Output render
				raytracing_x = 0;
				raytracing_y = 0;

				printf ("Output to file started...\n");
			}
			break;
		case 't': 
			// visualize the ray tree for the pixel at the current mouse position
			i = x;
			j = glutGet(GLUT_WINDOW_HEIGHT)-y;
			RayTree::Activate();
			raytracing_skip = 1;
			TraceRay(i,j);
			RayTree::Deactivate();

			// redraw
			Render();
			break;
		// VISUALIZATIONS
		case 'e': 
			// output current camera position (eye)
			std::cout << *mesh->getCamera() << std::endl;
			break;
		case 'q':
			// quit
			delete GLCanvas::raytracer;
			delete GLCanvas::radiosity;
			delete GLCanvas::mesh;
			exit(0);
			break;
		// MENU OPTIONS
		case 'w':
			if (optionID > 0)
			{
				optionID--;
				displayText();
			}
			break;
		case 's':
			if (optionID < 6)
			{
				optionID++;
				displayText();
			}
			break;
		case 'd':
			switch (optionID)
			{
				case BOUNCE:
					args->num_bounces++;
					break;
				case SHADOW:
					args->num_shadow_samples++;
					break;
				case MOTION:
					args->num_motion_samples++;
					args->timeStep = (0.3 / args->num_motion_samples);
					args->adaptive_antialiasing = false;
					break;
				case AA:
					args->adaptive_antialiasing = true;
					break;
				case DOF_S:
					args->num_dof_samples++;
					break;
				case DOF_BR:
					args->dof_blur_radius += 0.01f;
					break;
				case DOF_FL:
					args->dof_focal_length += 0.1f;
					break;
			}
			displayText();
			break;
		case 'a':
			switch (optionID)
			{
				case BOUNCE:
					if (args->num_bounces > 0) { args->num_bounces--; }
					break;
				case SHADOW:
					if (args->num_shadow_samples > 1 ) { args->num_shadow_samples--; }
					break;
				case MOTION:
					if (args->num_motion_samples > 0)
					{
						args->num_motion_samples--;
						args->timeStep = (0.3 / args->num_motion_samples);
						args->adaptive_antialiasing = false;
					}
					args->timeStep = (0.3 / args->num_motion_samples);
					break;
				case AA:
					args->adaptive_antialiasing = false;
					break;
				case DOF_S:
					if (args->num_dof_samples > 0) { args->num_dof_samples--; }
					break;
				case DOF_BR:
					if (args->dof_blur_radius > 0.01f) { args->dof_blur_radius -= 0.01f; }
					break;
				case DOF_FL:
					if (args->dof_focal_length > 0.1f) { args->dof_focal_length -= 0.1f; }
					break;
			}
			displayText();
			break;
		default:
			printf("UNKNOWN KEYBOARD INPUT  '%c'\n", key);
	}
}

Vec3f GLCanvas::TraceRaySubPixel(double x, double y, double time, int depth)
{
	Vec3f colorSamples[4];
	Vec3f positions[4];
	Ray rays[4];
	Vec3f samples = Vec3f(0.0, 0.0, 0.0);
	Hit hit;
	int i;

	double offsetx = 1.0/(args->width*4.0*pow(2.0, depth));
	double offsety = 1.0/(args->height*4.0*pow(2.0, depth));

	positions[0] = Vec3f(x + offsetx, y + offsety, 0.0);
	positions[1] = Vec3f(x + offsetx, y - offsety, 0.0);
	positions[2] = Vec3f(x - offsetx, y - offsety, 0.0);
	positions[3] = Vec3f(x - offsetx, y + offsety, 0.0);

	for (i=0; i < 4; i++)
	{
		rays[i] = mesh->getCamera()->generateRay(positions[i].x(), positions[i].y());

		colorSamples[i] = raytracer->TraceRay(rays[i], hit, time, 0, false);
		samples += colorSamples[i];
	}

	samples *= 0.25;

	if (depth == 0)
	{
		bool adaptiveSample = false;

		for (i=0; i < 4; i++)
		{
			if (abs(colorSamples[i].Length() - samples.Length()) > 0.3)
			{
				colorSamples[i] = TraceRaySubPixel(positions[i].x(), positions[i].y(), time, depth+1);
				adaptiveSample = true;
			}
		}

		if (adaptiveSample)
		{
			samples = Vec3f(0,0,0);

			for (i=0; i < 4; i++)
			{
				samples += colorSamples[i];
			}

			samples *= 0.25;
		}
	}
	
	RayTree::AddMainSegment(rays[0], 0, hit.getT());

	return samples;
}

// trace a ray through pixel (i,j) of the image an return the color
Vec3f GLCanvas::TraceRay(int i, int j)
{
	double x = (i + 0.5 - args->width/2.0)/max_d+0.5;
	double y = (j + 0.5 - args->height/2.0)/max_d+0.5;

	Vec3f motionSamples = Vec3f(0,0,0);

	if (args->adaptive_antialiasing)
	{
		return TraceRaySubPixel(x, y, 0.0, 0);
	}
	else if (args->num_motion_samples > 0)
	{
		double numSamples = args->num_motion_samples + 1;
		Vec3f motionSamples = Vec3f(0,0,0);

		Ray r = mesh->getCamera()->generateRay(x, y);
		Hit hit;

		for (double m=0; m < numSamples; m++)
		{
			motionSamples += raytracer->TraceRay(r, hit, m * args->timeStep, 0, false);
		}

		return (motionSamples * (1.0 / numSamples));
	}
	else
	{
		Ray r = mesh->getCamera()->generateRay(x, y);
		Hit hit;

		if (args->num_dof_samples == 0)
		{
			return raytracer->TraceRay(r, hit, 0.0, 0, false);
		}

		Vec3f sphereCenter = r.pointAtParameter(1);
		Vec3f pointNet = r.pointAtParameter(args->dof_focal_length);

		Vec3f depthSamples(0,0,0);

		for (int d=0; d < args->num_dof_samples; d++)
		{
			Vec3f pointOnSphere = sphereCenter + RandomUnitVector() * args->dof_blur_radius;
			Vec3f rayDirection = pointNet - pointOnSphere;
			rayDirection.Normalize();

			Ray depthRay(pointOnSphere + rayDirection * 0.001, rayDirection);
			depthSamples += raytracer->TraceRay(depthRay, hit, 0.0, 0, false);
		}

		depthSamples *= (1.0 / args->num_dof_samples);

		return depthSamples;
	}
}

// Scan through the image from the lower left corner across each row
// and then up to the top right.  Initially the image is sampled very
// coarsely.  Increment the static variables that track the progress
// through the scans
int GLCanvas::DrawPixel() {
	if (raytracing_x >= args->width)
	{
		raytracing_x = raytracing_skip/2;
		raytracing_y += raytracing_skip;
	}
	if (raytracing_y >= args->height)
	{
		if (raytracing_skip == 1)
		{
			end = clock();
			return 0;
		}

		raytracing_skip = raytracing_skip / 2;

		if (raytracing_skip % 2 == 0) raytracing_skip++;

		assert (raytracing_skip >= 1);
		raytracing_x = raytracing_skip/2;
		raytracing_y = raytracing_skip/2;
		glEnd();
		glPointSize(raytracing_skip);
		glBegin(GL_POINTS);
	}

	// compute the color and position of intersection
	Vec3f color= TraceRay(raytracing_x, raytracing_y);

	double r = linear_to_srgb(color.x());
	double g = linear_to_srgb(color.y());
	double b = linear_to_srgb(color.z());
	glColor3f(r,g,b);
	double x = 2 * (raytracing_x/double(args->width)) - 1;
	double y = 2 * (raytracing_y/double(args->height)) - 1;
	glVertex3f(x,y,-1);
	raytracing_x += raytracing_skip;
	return 1;
}

void GLCanvas::idle()
{
	if (args->raytracing_animation)
	{
		// draw 100 pixels and then refresh the screen and handle any user input
		glDisable(GL_LIGHTING);
		glDrawBuffer(GL_FRONT);
		glDisable(GL_DEPTH_TEST);
		glMatrixMode(GL_MODELVIEW);
		glLoadIdentity();
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glPointSize(raytracing_skip);
		glBegin(GL_POINTS);

		for (int i = 0; i < 100; i++)
		{
			if (!DrawPixel())
			{
				args->raytracing_animation = false;
				printf("Render time: %g Seconds\n\n", double(end - start)/CLOCKS_PER_SEC);
				break;
			}
		}

		glEnd();
		glFlush();
	}

	if (args->raytracing_output)
	{
		int i, j;
		double r, g, b;
		char progressBar[] = { '|','/', '-', '\\', };
		int pIndex = 0;

		double percent = args->height / 100.0;

		for (i=0; i < args->height; i++)
		{
			for (j=0; j < args->width; j++)
			{
				// compute the color and position of intersection
				Vec3f color= TraceRay(j, i);

				r = std::min<int>(linear_to_srgb(color.x()) * 255, 255);
				g = std::min<int>(linear_to_srgb(color.y()) * 255, 255);
				b = std::min<int>(linear_to_srgb(color.z()) * 255, 255);

				image.SetPixel(j, i, Color(r, g, b));
			}

			if (i % 5 == 0)
			{
				printf("\rProgress %g%%\t%c", i / percent, progressBar[pIndex]);
				pIndex++;
				if (pIndex == 4)
				{
					pIndex = 0;
				}
			}
		}

		printf("\rFinished!           ");

		args->raytracing_output = false;
		end = clock();

		printf("\nRender time: %g Seconds\n", double(end - start)/CLOCKS_PER_SEC);

		printf("Saving image!\n\n");
		image.Save("output.ppm");
	}
}


void GLCanvas::Render()
{
	glNewList(display_list_index, GL_COMPILE_AND_EXECUTE);
	// =========================================================
	// put your GL drawing calls inside the display list for efficiency
	radiosity->Paint(args);
	// Draw the ray tree
	glDisable(GL_LIGHTING);
	RayTree::paint();
	glEnable(GL_LIGHTING);

	// setup ortho view to draw text
	glMatrixMode(GL_PROJECTION);

	glOrtho(0, args->width, args->height, 0, -1, 1);
	glMatrixMode(GL_MODELVIEW);

	displayText();
		
	// =========================================================
	glEndList();
	glutPostRedisplay();
}


// ========================================================
// ========================================================

int HandleGLError() {
  GLenum error;
  int i = 0;
  while ((error = glGetError()) != GL_NO_ERROR) {
    printf ("GL ERROR(%d):  %s\n", i, gluErrorString(error));
    i++;
  }
  if (i == 0) return 1;
  return 0;
}
